/* Licensed to the Apache Software Foundation (ASF) under one or more
 * contributor license agreements.  See the NOTICE file distributed with
 * this work for additional information regarding copyright ownership.
 * The ASF licenses this file to You under the Apache License, Version 2.0
 * (the "License"); you may not use this file except in compliance with
 * the License.  You may obtain a copy of the License at
 *
 *     http://www.apache.org/licenses/LICENSE-2.0
 *
 * Unless required by applicable law or agreed to in writing, software
 * distributed under the License is distributed on an "AS IS" BASIS,
 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
 * See the License for the specific language governing permissions and
 * limitations under the License.
 */

/**
 * @file session.c
 * pyhp session support
 */

#include <time.h>
#include <stdlib.h>

#include <apr_strings.h>
#include <apr_md5.h>

#include "session.h"
#include "mod_pyhp.h"
#include "pyhp_layer.h"
#include "config.h"
#include "cookies.h"

#include "db_session.h"
#include "file_session.h"

/**
 * Python method to Save the session on file
 */
static PyObject *pyhp_session_save(PyObject *self, PyObject *args)
{
    PyObject *pyreq  = PyDict_GetItemString (self, "apache_request");
    request_rec *r = PyCObject_AsVoidPtr(pyreq);

    PyObject *py_pydata = PyDict_GetItemString (self, "pydata");
    PyHPData *pydata = PyCObject_AsVoidPtr(py_pydata);

    if ((!pydata) || (py_pydata == Py_None)) {
        pyhp_log(r, PyHP_ERR, "Failed to access PyData while saving session");
        return NULL;
    }

    PyObject * sess_mod = PyDict_GetItemString(self, "__session");
    char *id = PyString_AsString(PyDict_GetItemString(sess_mod, "id"));
    char *session_content = pyhp_layer_pack_object(pydata, PyDict_GetItemString(sess_mod, "data"));

    if (pyhp_config_sessbackend(r) == PyHPSessionDb) {
        if (!pyhp_session_db_save(pydata, id, session_content))
            return NULL;
    }
    else {
        if (!pyhp_session_file_save(pydata, id, session_content))
            return NULL;
    }

    Py_INCREF (Py_None);
    return Py_None;
}

/**
 * Python method to Wipe the session
 */
static PyObject *pyhp_session_wipe(PyObject *self, PyObject *args)
{
    PyObject *req  = PyDict_GetItemString (self, "apache_request");
    request_rec *r = PyCObject_AsVoidPtr(req);

    PyObject * sess_mod = PyDict_GetItemString(self, "__session");
    PyObject * sess_id = PyDict_GetItemString(sess_mod, "id");
    char *id = PyString_AsString(sess_id);

    PyObject *py_pydata = PyDict_GetItemString (self, "pydata");
    PyHPData *pydata = PyCObject_AsVoidPtr(py_pydata);

    if ((!pydata) || (py_pydata == Py_None)) {
        pyhp_log(r, PyHP_ERR, "Failed to access PyData while wiping session");
        return NULL;
    }

    if (pyhp_config_sessbackend(r) == PyHPSessionDb)
        pyhp_session_db_wipe(pydata, id);
    else
        pyhp_session_file_wipe(pydata, id);

    PyDict_SetItemString(sess_mod, "id", Py_None);

    Py_INCREF (Py_None);
    return Py_None;
}

static int pyhp_check_if_session_already_exists(request_rec *r, char const *sess_id)
{
    PyHPData *pydata;
    apr_pool_userdata_get((void **)&pydata, "pyhp_data", r->pool);

    if (pyhp_config_sessbackend(r) == PyHPSessionDb)
        return pyhp_session_db_check_if_exists(pydata, sess_id);
    else
        return pyhp_session_file_check_if_exists(pydata, sess_id);
}

char *pyhp_session_gen(request_rec *r)
{
    apr_md5_ctx_t id;
    char const *tmp;
    char digest[APR_MD5_DIGESTSIZE];
    time_t now;
    int salt;

    char * session_id = apr_palloc ( r->pool, 33 );
    int t;
    char buf[4];

    do {
        now = time(NULL);
        salt = rand();

        apr_md5_init(&id);
        tmp = apr_table_get(r->headers_in, "Host");
        if (tmp)
            apr_md5_update(&id, tmp, strlen(tmp));
        tmp = apr_table_get(r->headers_in, "User-Agent");
        if (tmp)
            apr_md5_update(&id, tmp, strlen(tmp));
        apr_md5_update(&id, (char*)&now, sizeof(time_t));
        apr_md5_update(&id, (char*)&salt, sizeof(int));

        apr_md5_final(digest, &id);
        strcpy ( session_id, "" );

        for ( t = 0; t < 16; t ++ ) {
                sprintf ( buf, "%2.2x", ( unsigned char ) digest [ t ] );
                strcat ( session_id, buf );
        }

    } while ( pyhp_check_if_session_already_exists ( r, session_id ) );

    return session_id;
}

/**
 * Checks if session start has been called and if the session is still valid
 * @see pyhp_session_start
 */
static int pyhp_check_if_session_has_been_created(PyHPData *pydata, request_rec *r)
{
    PyObject *session = pyhp_layer_get_object(pydata, "session");
    PyObject *session_dict = PyModule_GetDict(session);
    PyObject *session_id = PyDict_GetItemString(session_dict, "id");

    return ((session_id) && (session_id != Py_None));
}

/**
 * Python method to start a new session.
 * The session id is generated on the md5 of the Host and
 * User-Agent headers and on the current time.
 */
static PyObject *pyhp_session_start(PyObject *self, PyObject *args)
{
    PyObject *py_pydata = PyDict_GetItemString (self, "pydata");
    PyHPData *pydata = PyCObject_AsVoidPtr(py_pydata);

    PyObject *req  = PyDict_GetItemString (self, "apache_request");
    request_rec *r = PyCObject_AsVoidPtr(req);

    if (!pyhp_check_if_session_has_been_created(pydata, r)) {
        char *session_id = pyhp_session_gen(r);
        PyObject * sess_id = PyString_FromString(session_id);
        PyObject * sess_mod = PyDict_GetItemString(self, "__session");

        PyDict_SetItemString(sess_mod, "id", sess_id);
        return sess_id;
    }

    Py_INCREF(Py_None);
    return Py_None;
}

static PyMethodDef session_method_table[] = {
    { "save", (PyCFunction)pyhp_session_save, METH_NOARGS,
                                                          "Save the session" },
    { "wipe", (PyCFunction)pyhp_session_wipe, METH_NOARGS,
                                                     "Wipe the saved session"},
    { "start", (PyCFunction)pyhp_session_start, METH_NOARGS,
                                                     "Starts a session"},
    { NULL, NULL }
};

/**
 * Loads a session from file basing on the current Session cookie.
 * if it is not possible to load the session an empty one will
 * be created.
 * The method is called on every page request.
 */
void pyhp_session_load(PyHPData *pydata, request_rec *r)
{
    PyObject *session = pyhp_layer_get_object(pydata, "session");
    PyObject *session_dict = PyModule_GetDict(session);
    PyObject *session_id = pyhp_cookies_get_session_cookie(pydata, r);
    PyObject *session_data;

    if (session_id && (pyhp_check_if_session_already_exists(r, PyString_AsString(session_id)) != 0)) {
        char *c_session_id = PyString_AsString(session_id);

        if (pyhp_config_sessbackend(r) == PyHPSessionDb)
            session_data = pyhp_session_db_load(pydata, c_session_id);
        else
            session_data = pyhp_session_file_load(pydata, c_session_id);

        PyDict_SetItemString(session_dict, "id", session_id);
        Py_XDECREF(session_id);

        if (session_data)
            pyhp_logf(r, PyHP_DEBUG, "Loaded session %s", c_session_id);
        else {
            session_data = PyDict_New();
            pyhp_logf(r, PyHP_ERR, "Unable to load session %s, creating an empty one", c_session_id);
        }
    }
    else {
        PyDict_SetItemString(session_dict, "id", Py_None);
        Py_XDECREF(Py_None);

        session_data = PyDict_New();
    }

    PyDict_SetItemString(session_dict, "data", session_data);
    Py_XDECREF(session_data);
}

/**
 * Really writes down the session on a file and creates a cookie
 * for the client to remember the session id.
 * The method is called on every request at the end of the request execution
 */
void pyhp_session_write(PyHPData * pydata, request_rec * r)
{
    PyObject *session = pyhp_layer_get_object(pydata, "session");
    PyObject *session_dict = PyModule_GetDict(session);

    PyObject *session_id = PyDict_GetItemString(session_dict, "id");
    if (session_id != Py_None) {
        pyhp_log(r, PyHP_DEBUG, "Writing session...");

        pyhp_session_save(PyDict_GetItemString(session_dict, "__internals"), NULL);
        pyhp_cookies_create_session_cookie(pydata, session_id, r);
    }
}


/**
 * Exports the session constants and methods to the python layer
 * @see pyhp_layer_init
 */
void pyhp_session_export(PyObject *dict, PyObject *self)
{
    PyObject *sess = Py_InitModule4("pyhp_session", session_method_table,
                                     "", self, PYTHON_API_VERSION );
    PyDict_SetItemString(dict, "session", sess );
    Py_XDECREF(sess);

    dict = PyModule_GetDict(sess);
    PyDict_SetItemString(dict, "__internals", self);
    PyDict_SetItemString(self, "__session", dict);
    Py_XDECREF(self);
}



